﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace WcfContrib.Extensions.Caching
{
    /// <summary>
    /// Represents the key that is attached for a service operation distinguishing calls from one another
    /// </summary>
    public class OutputCacheItemKey : IEquatable<OutputCacheItemKey>
    {
        static readonly Func<object, int> StandardGetHashCode = (obj => obj.GetHashCode());
        int? calculatedHash;
        bool hasComparers;
        string calculatedString;

        /// <summary>
        /// Gets the operation action.
        /// </summary>
        /// <value>The operation action.</value>
        public string Action { get; private set; }
        /// <summary>
        /// Gets a value indicating whether to compare IEnumerable inline items.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if to compare IEnumerable inline items; otherwise, <c>false</c>.
        /// </value>
        public bool CompareEnumerableItems { get; private set; }
        /// <summary>
        /// Gets or sets the operation parameter values.
        /// </summary>
        /// <value>The operation parameter values.</value>
        public object[] Parameters { get; private set; }
        /// <summary>
        /// Gets the index-based comparers used to distinguish between pazrameter instances.
        /// </summary>
        /// <value>The index-based comparers used to distinguish between pazrameter instances.</value>
        public Dictionary<int, IEqualityComparer<object>> Comparers { get; private set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="OutputCacheItemKey"/> class.
        /// </summary>
        /// <param name="action">The operation action.</param>
        /// <param name="compareEnumerableItems">if set to <c>true</c> compare IEnumerable inline items.</param>
        /// <param name="parameters">The operation parameter values.</param>
        /// <param name="comparers">The index-based comparers used to distinguish between pazrameter instances.</param>
        public OutputCacheItemKey(string action, bool compareEnumerableItems, object[] parameters, Dictionary<int, IEqualityComparer<object>> comparers)
        {
            this.Action = action;
            this.CompareEnumerableItems = compareEnumerableItems;
            this.Parameters = parameters;
            this.Comparers = comparers;

            hasComparers = Comparers != null && Comparers.Count > 0;
        }

        #region IEquatable<ResultCacheKey> Members

        int CustomGetHashCode(object obj)
        {
            IEnumerable enumerable = obj as IEnumerable;
            
            if (enumerable != null)
            {
                int? hash = enumerable.Cast<object>()
                    .Select(i => i == null ? null : new int?(StandardGetHashCode(i)))
                    .Where(h => h.HasValue)
                    .Aggregate((h1, h2) => h1.Value ^ h2.Value);

                return hash.GetValueOrDefault();
            }
            else
                return StandardGetHashCode(obj);
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
        /// </returns>
        public override int GetHashCode()
        {
            if (calculatedHash.HasValue)
            {
                return calculatedHash.Value;
            }

            calculatedHash = Action.GetHashCode();
            for (int i = 0; i < this.Parameters.Length; i++)
            {
                object parameter = this.Parameters[i];

                if (parameter != null)
                {
                    Func<object, int> getHashCode = null;

                    IEqualityComparer<object> comparer;
                    if (hasComparers && this.Comparers.TryGetValue(i, out comparer))
                    {
                        getHashCode = comparer.GetHashCode;
                    }
                    else
                    {
                        getHashCode = CompareEnumerableItems
                            ? CustomGetHashCode
                            : StandardGetHashCode;
                    }

                    calculatedHash = calculatedHash.Value ^ getHashCode(parameter);
                }
            }

            return calculatedHash.Value;
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        /// 	<c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref="T:System.NullReferenceException">
        /// The <paramref name="obj"/> parameter is null.
        /// </exception>
        public override bool Equals(object obj)
        {
            OutputCacheItemKey key = obj as OutputCacheItemKey;

            if (key != null)
            {
                return Equals(key);
            }
            
            return false;
        }

        static bool CustomEquals(object a, object b)
        {
            IEnumerable ieA, ieB;
            ieA = a as IEnumerable;
            ieB = b as IEnumerable;

            if (ieA != null && ieB != null)
            {
                return ieA.Cast<object>().SequenceEqual(ieB.Cast<object>());
            }
            else
            {
                return Object.Equals(a, b);
            }
        }

        /// <summary>
        /// Indicates whether the current object is equal to another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
        /// </returns>
        public bool Equals(OutputCacheItemKey other)
        {
            if (!Action.Equals(other.Action))
            {
                return false;
            }

            for (int i = 0; i < this.Parameters.Length; i++)
            {
                Func<object, object, bool> compare;
                
                IEqualityComparer<object> comparer;
                if (hasComparers && this.Comparers.TryGetValue(i, out comparer))
                {
                    compare = comparer.Equals;
                }
                else if (this.CompareEnumerableItems)
                {
                    compare = CustomEquals;
                }
                else
                {
                    compare = Object.Equals;
                }

                if (!compare(this.Parameters[i], other.Parameters[i]))
                {
                    return false;
                }
            }

            return true;
        }

        #endregion

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            if (calculatedString == null)
            {
                calculatedString = Action;

                calculatedString += Parameters
                    .Select(p => p == null ? string.Empty : p.ToString())
                    .Aggregate((p1, p2) => p1 + p2);
            }

            return calculatedString;
        }
    }
}
